﻿using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Diagnostics;
using System.Text;
using Lilium.ContainerModel;
using Microsoft.Practices.Mobile.DataAccess;

namespace Lilium.RetailAudit.Data
{
	/// <summary>
	/// Provides access to persistent storage.
	/// </summary>
	/// <typeparam name="TEntity"></typeparam>
	public sealed class Repository<TEntity> : IRepository<TEntity>
		where TEntity : EntityBase<TEntity>
	{
		public Repository(IResolver resolver)
		{
			if (resolver == null) throw new ArgumentNullException("resolver");

			var entityType = typeof(TEntity);
			var metaModelProvider = resolver.Resolve<MetaModelProvider>();
			var metaTable = metaModelProvider.GetMetaTable(entityType);

			var createQueryBuilder = new StringBuilder("INSERT INTO " + metaTable.Name);
			var createQueryParamterActions = new List<Func<Database, List<DbParameter>, TEntity, string>>();
			
			var selectQueryBuilder = new StringBuilder("SELECT ");

			var readFieldActions = new List<Action<DbDataReader, TEntity>>();
			var fieldsCount = 0;
			foreach (var prop in entityType.GetProperties())
			{
				if (fieldsCount != 0)
				{
					createQueryBuilder.Append(", ");
					selectQueryBuilder.Append(", ");
				}
				else
				{
					createQueryBuilder.Append("(");
				}
				selectQueryBuilder.Append(prop.Name);
				createQueryBuilder.Append(prop.Name);
				var context = new
				{ // зохват контекста работает только так.
					fieldIndex = fieldsCount,
					propertyInfo = prop
				};
				readFieldActions.Add((reader, entity) =>
				{
					if (!reader.IsDBNull(context.fieldIndex))
						context.propertyInfo.SetValue(entity, reader.GetValue(context.fieldIndex), null);
				});
				createQueryParamterActions.Add((database, parameters, entity) =>
				{
					var value = context.propertyInfo.GetValue(entity, null);
					var parameter = database.CreateParameter("P" + context.fieldIndex.ToString(), value);
					parameters.Add(parameter);
					return parameter.ParameterName;
				});
				fieldsCount++;
			}
			if (fieldsCount == 0)
				throw new InvalidOperationException(string.Format(
					"Entity of type {0} does not has public properties",
					entityType));
			selectQueryBuilder.Append(" FROM ");
			selectQueryBuilder.Append(metaTable.Name);
			createQueryBuilder.Append(")");
			var createQuery = createQueryBuilder.ToString();
			m_CreateFunc = entity =>
			{

				var insertQueryBuilder = new StringBuilder(createQuery);
				var database = Resolver.Resolve<Database>();
				var parameters = new List<DbParameter>();
				var hasParameters = false;
				foreach (var action in createQueryParamterActions)
				{
					if (!hasParameters)
					{
						insertQueryBuilder.Append(" VALUES (");
						hasParameters = true;
					}
					else
						insertQueryBuilder.Append(", ");
					var paramName = action(database, parameters, entity);
					insertQueryBuilder.Append(paramName);
				}
				insertQueryBuilder.Append(")");
				database.ExecuteNonQuery(insertQueryBuilder.ToString(), parameters.ToArray());
			};
			m_CountQuery = "SELECT COUNT(*) FROM " + metaTable.Name;
			m_DeleteQuery = "DELETE FROM " + metaTable.Name;
			m_ReadEntityFunc = reader =>
			{
				var entity = Activator.CreateInstance<TEntity>();
				foreach (var readFieldAction in readFieldActions)
					readFieldAction(reader, entity);
				return entity;
			};
			m_Resolver = resolver;
			m_SelectQuery = selectQueryBuilder.ToString();
			m_MetaTable = metaTable;
		}

		public int Count(IEnumerable<object> disjuncts)
		{
			var database = Resolver.Resolve<Database>();
			var parameters = new List<DbParameter>();
			var queryBuilder = CreateDisjunctiveQuery(database, CountQuery, disjuncts, parameters);
			return (int)database.ExecuteScalar(queryBuilder.ToString(), parameters.ToArray());
		}

		public void Create(TEntity entity)
		{
			entity.OnInsert();
			CreateFunc(entity);
		}

		public void Delete(IEnumerable<object> disjuncts)
		{
			var database = Resolver.Resolve<Database>();
			var parameters = new List<DbParameter>();
			var queryBuilder = CreateDisjunctiveQuery(database, DeleteQuery, disjuncts, parameters);
			database.ExecuteNonQuery(queryBuilder.ToString(), parameters.ToArray());
		}

		public IEnumerable<TEntity> Select(IEnumerable<object> disjuncts)
		{
			var database = Resolver.Resolve<Database>();
			var parameters = new List<DbParameter>();
			var queryBuilder = CreateDisjunctiveQuery(database, SelectQuery, disjuncts, parameters);
			if (!string.IsNullOrEmpty(MetaTable.DefaultSortExpression))
			{
				queryBuilder.Append(" ORDER BY ");
				queryBuilder.Append(MetaTable.DefaultSortExpression);
			}
			using (var reader = database.ExecuteReader(queryBuilder.ToString(), parameters.ToArray()))
				while (reader.Read())
					yield return ReadEntityFunc(reader);
		}

		public void Update(TEntity entity)
		{
			entity.OnUpdate();
			throw new NotImplementedException();
		}

		private static StringBuilder CreateDisjunctiveQuery(Database database, string baseQuery, IEnumerable<object> disjuncts, List<DbParameter> parameters)
		{
			if (database == null) throw new ArgumentNullException("database");
			if (baseQuery == null) throw new ArgumentNullException("baseQuery");
			if (disjuncts == null) throw new ArgumentNullException("disjuncts");
			if (parameters == null) throw new ArgumentNullException("parameters");

			var queryBuilder = new StringBuilder(baseQuery);
			var hasWhereCondition = false;
			foreach (var disjunct in disjuncts)
			{
				var hasConjuncts = false;
				foreach (var propertyInfo in disjunct.GetType().GetProperties())
				{
					if (!hasWhereCondition)
					{
						queryBuilder.Append(" WHERE ");
						hasWhereCondition = true;
					}
					else
					{
						if (!hasConjuncts)
							queryBuilder.Append(" OR ");
					}
					if (!hasConjuncts)
					{
						queryBuilder.Append("(");
						hasConjuncts = true;
					}
					else
						queryBuilder.Append(" AND ");
					queryBuilder.Append(propertyInfo.Name);
					var value = propertyInfo.GetValue(disjunct, null);
					if (value != null)
					{
						queryBuilder.Append(" = ");
						var parameter = database.CreateParameter("p" + parameters.Count.ToString(), value);
						queryBuilder.Append(parameter.ParameterName);
						parameters.Add(parameter);
					}
					else
					{
						queryBuilder.Append(" IS NULL");
					}
				}
				if (hasConjuncts)
					queryBuilder.Append(")");
			}
			return queryBuilder;
		}

		#region private string CountQuery

		private readonly string m_CountQuery;

		private string CountQuery
		{
			[DebuggerNonUserCodeAttribute]
			get
			{
				return m_CountQuery;
			}
		}

		#endregion

		#region public Action<TEntity> CreateFunc

		private readonly Action<TEntity> m_CreateFunc;

		public Action<TEntity> CreateFunc
		{
			[DebuggerNonUserCodeAttribute]
			get
			{
				return m_CreateFunc;
			}
		}

		#endregion

		#region private string DeleteQuery

		private readonly string m_DeleteQuery;

		private string DeleteQuery
		{
			[DebuggerNonUserCodeAttribute]
			get
			{
				return m_DeleteQuery;
			}
		}

		#endregion
		
		#region public MetaTable MetaTable

		private readonly MetaTable m_MetaTable;

		public MetaTable MetaTable
		{
			[DebuggerNonUserCodeAttribute]
			get
			{
				return m_MetaTable;
			}
		}

		#endregion

		#region private Func<DbDataReader, TEntity> ReadEntityFunc

		private readonly Func<DbDataReader, TEntity> m_ReadEntityFunc;

		private Func<DbDataReader, TEntity> ReadEntityFunc
		{
			[DebuggerNonUserCodeAttribute]
			get
			{
				return m_ReadEntityFunc;
			}
		}

		#endregion

		#region private IResolver Resolver

		private readonly IResolver m_Resolver;

		private IResolver Resolver
		{
			[DebuggerNonUserCodeAttribute]
			get
			{
				return m_Resolver;
			}
		}

		#endregion

		#region private static string SelectQuery

		private readonly string m_SelectQuery;

		private string SelectQuery
		{
			[DebuggerNonUserCodeAttribute]
			get
			{
				return m_SelectQuery;
			}
		}

		#endregion
	}
}